refactor(plugin-chatbot): drop the inert blanket cast on useChat's options object - #8401
Merged
Merged
Conversation
…options object
`useChat({...} as any)` widened the whole options argument to `any`, switching
off checking of `transport`, `onError` and excess properties, and blocking
inference of `UI_MESSAGE`. Measured: with the nested `(aiInitialMessages as any)`
in place, the outer cast hides nothing — both type-check projects stay at exit 0
with zero diagnostics without it — and the resolved instantiation is unchanged,
`UseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>` either way, because
`transport` is the only inference source and it is already pinned to that type.
This narrows the suppression rather than eliminating it: removing BOTH casts is
red (TS2322, `Record<string, unknown>` is not a `UIMessagePart`), so the nested
cast is the live one. A comment now records that, so the next reader does not
delete it blind or re-widen the call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…ng nothing Empty frontmatter, the gate's documented exemption. Argued from measurement: building @object-ui/plugin-chatbot from this branch and from the merge-base produced a byte-identical dist across all 34 published artifacts, and that comparison was proved live by a control mutation to an emitted string literal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #8378
Removes
useChat({...} as any)inpackages/plugin-chatbot/src/useObjectChat.ts, and records in a comment what the measurement actually found.Spelling note: generic type arguments are written as WORDS where they appear, because GitHub's body sanitizer eats tag-shaped fragments.
The site, re-derived
The brief said the file had moved twice and not to trust
:735. Re-derived by content, not by line number:grep -n 'as any'returns four hits, of which one (:368) is a doc comment naming the cast objectui#8342 removed and one (:808) is an unrelated metadata read. The site ispackages/plugin-chatbot/src/useObjectChat.ts:735, the line} as any);, unique in the file.:735was still correct. PR #8377 landed above the site but the intervening edits were net line-neutral at this point in the file. I re-derived it independently rather than trusting it, and the base read below confirms the line number as well as the content — but the dispatch's expectation that the number had moved was wrong.Base side read with a literal sha (
fadf6cd7317e1ac8a011f4e6b986187223c7fdba), interrogated in both directions so it cannot be an index read of my own work:} as any);SdkChatMessage(PR #8377 landed marker)} as any);Base blob
ff06f46f94c81cda552f741484e36e36c5df08fe, branch blob0c3c0da6259ba6725ccb6540a1185428d16a0372.The card asked whether the outer cast is inert on its own merits, or only because the nested
(aiInitialMessages as any)absorbs the mismatch. Measured as a 2x2, each leg mutated on disk (hash proven to differ from the HEAD blob), each restored by state:tsc --noEmittsc -p tsconfig.test.jsonThe bottom row is the answer:
So the outer cast is inert only because the nested one is absorbing a real mismatch. The suppression count on this call goes 2 to 1, not 2 to 0. The card's headline — "this cast is DEAD" — is true only conditionally, and the condition is that the nested cast stays. Row 3 shows the converse: with the nested cast gone the outer one would have been load-bearing.
It is still worth landing, for a reason that is about scope rather than count. The outer cast widened the entire options argument to
any:transport,onError, excess-property checking and the generic instantiation all went unchecked. The nested cast suppresses exactly one property. Removing the outer restores compiler checking of everything exceptmessages. That is real checking newly acquired, not zero — but it is a different claim from the one the card makes, and the card should be re-read in that light.Which instantiation results
The card asked this explicitly. Probed with the TypeScript compiler API against the real program (no mutation), before and after:
chatResulttypeUseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>anyUseChatHelpers<UIMessage<unknown, UIDataTypes, UITools>>{ transport: DefaultChatTransport<UIMessage<unknown, UIDataTypes, UITools>> or undefined; messages: any; onError: ((err: Error) => void) or undefined; }Identical, in both the main and the test program. Inference does start running, and it lands on exactly the type the default already supplied, so nothing downstream of
chatResultmoves.The declaration-level argument — and where it stops
useChatis declareduseChat<UI_MESSAGE extends UIMessage = UIMessage>(options?: UseChatOptions<UI_MESSAGE>): UseChatHelpers<UI_MESSAGE>, andUseChatOptionsis({ chat: Chat<UI_MESSAGE> } | ChatInit<UI_MESSAGE>) & { throttle?; experimental_throttle?; resume? }. Only three properties are passed, and each contributes toUI_MESSAGEinference as follows:onErroris declaredChatOnErrorCallback, and that istype ChatOnErrorCallback = (error: Error) => void— no type parameter occurs in it at all. This one is genuinely declaration-level and permanent: whatever is passed, this property can never produce an inference candidate forUI_MESSAGE.messagesisanyat the call, and ananysource contributes no inference candidate (it carries none of the object/intersection flags the inference walk requires). Declaration-level as a rule of the algorithm, but contingent on the nested cast staying.transportis therefore the sole inference source, and it isDefaultChatTransport<UIMessage<unknown, UIDataTypes, UITools>>— itself pinned there becausenew DefaultChatTransport({ ... })at :637 supplies no explicit type argument and no candidate either, leaving its ownUI_MESSAGEto fall back to its constraint.I cannot make the strong claim. "This cast is a no-op for every possible instantiation" is not true here, and saying it would be dressing the result up. The honest claim is the weaker one the brief asks for:
That weakness is why the deletion ships with a comment rather than silently: the next reader who removes the nested cast expecting the last suppression to fall away needs to know what they will hit, and needs to not "fix" it by re-widening the call.
Instrument discipline
Both projects, with membership proof.
type-checkfor this package istsc --noEmit && tsc -p tsconfig.test.json; both were run separately for every leg.tsconfig.jsonexcludes tests by directory, andtsconfig.test.jsonincludes only*.test.ts(x), so membership was not assumed:--listFiles | grep -c 'src/useObjectChat\.ts$'= 1; lit control (a made-up filename on the same instrument) = 0useObjectChat.*suites, named in the run)The instrument is not merely present but demonstrably able to fail: row 4 turned both programs red on this exact file.
Restore-integrity / instrument-liveness control. Re-inserting the cast onto the implementation returns both projects to exit 0. Reported as what it is — evidence the tree is unbroken and the harness restores correctly — not as a second reading about the cast, since it restores code
mainkeeps green by construction.Dependents, downstream direction.
pnpm --workspace-concurrency=2 --filter '...@object-ui/plugin-chatbot' type-check, after building the full closure first so a staledistcould not masquerade as a verdict (the TS2307/TS2882 signature objectui#8342 hit).Zero
error TSacross the run; per package:packages/plugin-chatbotDone,packages/app-shellDone,apps/consoleDone,apps/siteDone,examples/schema-catalogDone,examples/console-starterDone,examples/byo-backend-consoleDone.Vitest. Run from the repo root as AGENTS.md requires (
pnpm exec vitest run packages/plugin-chatbot/), which reportsRUN v4.1.10 /home/user/objectui-issue-8378— the repo root, not a package dir. 40 test files passed, 474 tests passed.vitest listnames all sixuseObjectChat.*suites and shows zeroapps/consolefiles, so this is not the objectui#3378 false green. As the brief notes, vitest transpiles types away and can never see a type-level change; it is here to show nothing moved.Lint is the package-scoped
eslint .: exit 0,88 problems (0 errors, 88 warnings). Against the base copy of the same file on the same instrument:89 problems (0 errors, 89 warnings), withno-explicit-anyhits going 13 to 12. The deleted cast was a real reportedany, and exactly one warning went away.Mutation hygiene. Every leg: absolute-path
trap ... EXIT INT TERM, mutation proved on disk bygit hash-objectdiffering fromgit rev-parse HEAD:PATH(empty hash guarded as failure), anchor counted in both directions (deleted text 1 to 0, injected text 1 to 2), restore verified by state — hash equal again andgit diff HEADempty — never by an exit code. The ablation legs ran from a committed implementation.Changeset
node scripts/check-changeset-presence.mjsdecided it, and its verdict line is:Empty frontmatter, argued the way PR #8389 argued it — from a measurement, not by feel. Building
@object-ui/plugin-chatbotfrom this branch and from the merge-base produced a byte-identicaldistacross all 34 published artifacts (.d.ts,.js,.cjs,.css, compared bysha256summanifest).That "identical" verdict was itself proved live, because a comparison that can only ever say "identical" is not a measurement: a control mutation to an emitted string literal in the same file, verified to have reached
dist/index.jsbefore the comparison ran, made the same manifest report differing hashes forindex.jsandindex.umd.cjs. A first control attempt — an unused local const — was tree-shaken away and never reacheddist, and is reported here as void rather than as a dead instrument.scripts/check-changeset-no-major.mjs:✅ No changeset declares a 'major' bump.Noskip-changesetlabel was applied; it is a phantom label in this repo and a labelled bypass was declined twice.pnpm check:control-bytes:✅ check-control-bytes: OK (scanned 6653 tracked text file(s); skipped 85 binary), plus a directgrep -naPcontrol-byte scan of the edited file with a firing lit control.All readings above were re-taken at the final head
4a134d170with a clean working tree.Out of scope, and not filed — needs the PM
The nested
(aiInitialMessages as any)is now measured live, not merely suspected: it hides a genuine TS2322, because theaiInitialMessagesbuilder at :545 constructspartsasRecord<string, unknown>[], which is not aUIMessagePartunion. The real fix is at the producer (build realUIMessagePartvalues), not a wider cast at the consumer — contract-first, AGENTS.md #0.1.I could not file it as its own card: dedupe search was unavailable this session (MCP
search_issuesreturnedAPI rate limit already exceededon two attempts; repo-scoped REST returns 403GitHub access is not enabled for this session, an app-installation policy rather than a quota, while/rate_limititself answers 200). Filing without a dedupe read is not allowed, and dropping the finding silently is not allowed either, so it goes to the PM in the report to file.🤖 Generated with Claude Code
https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
Generated by Claude Code